﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data.Common;

namespace HobSoft.Utility.SqlGateway.Extensions
{
    internal class DbReaderExtensionsData
    {
        internal static ReadOnlyCollection<Type> DbDataReaderEnumType = new ReadOnlyCollection<Type>(
            new Type[] { typeof(byte), typeof(sbyte), typeof(Int16), typeof(UInt16), typeof(Int32), typeof(UInt32), typeof(Int64), typeof(UInt64) });

        internal static ReadOnlyDictionary<Type, Func<DbDataReader, int, object>> DbDataReaderCastMapping =
            new ReadOnlyDictionary<Type, Func<DbDataReader, int, object>>(
                new Dictionary<Type, Func<DbDataReader, int, object>>()
                {
                    { typeof(Int16), (dr, i) => dr.GetInt16(i)},
                    { typeof(Int32), (dr, i) => dr.GetInt32(i)},
                    { typeof(Int64), (dr, i) => dr.GetInt64(i)},
                    { typeof(UInt16), (dr, i) => (UInt16)dr.GetInt16(i)},
                    { typeof(UInt32), (dr, i) => (UInt32)dr.GetInt32(i)},
                    { typeof(UInt64), (dr, i) => (UInt64)dr.GetInt64(i)},
                    { typeof(byte), (dr, i) => dr.GetByte(i)},
                    { typeof(sbyte), (dr, i) => (sbyte)dr.GetByte(i)},
                    { typeof(bool), (dr, i) => dr.GetBoolean(i)},
                    { typeof(char), (dr, i) => dr.GetChar(i)},
                    { typeof(DateTime), (dr, i) => dr.GetDateTime(i)},
                    { typeof(decimal), (dr, i) => dr.GetDecimal(i)},
                    { typeof(double), (dr, i) => dr.GetDouble(i)},
                    { typeof(float), (dr, i) => dr.GetFloat(i)},
                    { typeof(Guid), (dr, i) => dr.GetGuid(i)},
                    { typeof(Int16?), (dr, i) => dr.GetInt16(i)},
                    { typeof(Int32?), (dr, i) => dr.GetInt32(i)},
                    { typeof(Int64?), (dr, i) => dr.GetInt64(i)},
                    { typeof(UInt16?), (dr, i) => (UInt16)dr.GetInt16(i)},
                    { typeof(UInt32?), (dr, i) => (UInt32)dr.GetInt32(i)},
                    { typeof(UInt64?), (dr, i) => (UInt64)dr.GetInt64(i)},
                    { typeof(byte?), (dr, i) => dr.GetByte(i)},
                    { typeof(sbyte?), (dr, i) => (sbyte)dr.GetByte(i)},
                    { typeof(bool?), (dr, i) => dr.GetBoolean(i)},
                    { typeof(char?), (dr, i) => dr.GetChar(i)},
                    { typeof(DateTime?), (dr, i) => dr.GetDateTime(i)},
                    { typeof(decimal?), (dr, i) => dr.GetDecimal(i)},
                    { typeof(double?), (dr, i) => dr.GetDouble(i)},
                    { typeof(float?), (dr, i) => dr.GetFloat(i)},
                    { typeof(Guid?), (dr, i) => dr.GetGuid(i)},
                    { typeof(string), (dr, i) => dr.GetString(i)}
                }
            );

        internal static ReadOnlyDictionary<Type, Func<object, object>> TypeConvertCastMapping = new ReadOnlyDictionary<Type, Func<object, object>>(
            new Dictionary<Type, Func<object, object>>(){
                { typeof(Int16), val => Convert.ToInt16(val)},
                { typeof(Int32), val => Convert.ToInt32(val)},
                { typeof(Int64), val => Convert.ToInt64(val)},
                { typeof(UInt16), val => Convert.ToUInt16(val)},
                { typeof(UInt32), val => Convert.ToUInt32(val)},
                { typeof(UInt64), val => Convert.ToUInt64(val)},
                { typeof(byte), val => Convert.ToByte(val)},
                { typeof(sbyte), val => Convert.ToSByte(val)},
                { typeof(bool), val => Convert.ToBoolean(val)},
                { typeof(char), val => Convert.ToChar(val)},
                { typeof(DateTime), val => Convert.ToDateTime(val)},
                { typeof(decimal), val => Convert.ToDecimal(val)},
                { typeof(double), val => Convert.ToDouble(val)},
                { typeof(float), val => Convert.ToSingle(val)},
                { typeof(Guid), val => Guid.Parse(val.ToString())},
                { typeof(Int16?), val => Convert.ToInt16(val)},
                { typeof(Int32?), val => Convert.ToInt32(val)},
                { typeof(Int64?), val => Convert.ToInt64(val)},
                { typeof(UInt16?), val => Convert.ToUInt16(val)},
                { typeof(UInt32?), val => Convert.ToUInt32(val)},
                { typeof(UInt64?), val => Convert.ToUInt64(val)},
                { typeof(byte?), val => Convert.ToByte(val)},
                { typeof(sbyte?), val => Convert.ToSByte(val)},
                { typeof(bool?), val => Convert.ToBoolean(val)},
                { typeof(char?), val => Convert.ToChar(val)},
                { typeof(DateTime?), val => Convert.ToDateTime(val)},
                { typeof(decimal?), val => Convert.ToDecimal(val)},
                { typeof(double?), val => Convert.ToDouble(val)},
                { typeof(float?), val => Convert.ToSingle(val)},
                { typeof(Guid?), val => Guid.Parse(val.ToString())},
                { typeof(string), val => val.ToString()}
            }
            );

        internal static bool CastEnum(DbDataReader reader, int ordinal, Type dbDataType, Type castType, out object result)
        {
            result = null;
            if (castType.IsEnum)
            {
                if (DbReaderExtensionsData.DbDataReaderEnumType.Contains(dbDataType))
                {
                    try
                    {
                        result = Enum.ToObject(castType, reader.GetValue(ordinal));
                        return true;
                    }
                    catch
                    {
                    }
                }
                else if (dbDataType == typeof(string))
                {
                    try
                    {
                        result = Enum.Parse(castType, reader.GetString(ordinal));
                        return true;
                    }
                    catch
                    {
                    }
                }
            }
            return false;
        }
    }

    public static class DbReaderExtensions
    {
        public static bool TryCast<T>(this DbDataReader reader, int ordinal, out T result)
        {
            result = default(T);
            if (!reader.HasRows || reader.IsDBNull(ordinal))
                return false;
            var type = reader.GetFieldType(ordinal);
            var castType = typeof(T);
            if (castType.IsEnum)
            {
                object enumValue;
                var castResult = DbReaderExtensionsData.CastEnum(reader, ordinal, type, castType, out enumValue);
                if (castResult)
                    result = (T)enumValue;
                return castResult;
            }

            if (type == castType)
            {
                try
                {
                    result = reader.GetFieldValue<T>(ordinal);
                    return true;
                }
                catch
                {
                    return false;
                }
            }
            var dbVal = reader.GetValue(ordinal);
            if (DbReaderExtensionsData.TypeConvertCastMapping.ContainsKey(castType))
            {
                try
                {
                    result = (T)DbReaderExtensionsData.TypeConvertCastMapping[castType](dbVal);
                    return true;
                }
                catch
                {
                    return false;
                }
            }
            try
            {
                result = (T)Convert.ChangeType(dbVal, castType);
                return true;
            }
            catch
            {
                return false;
            }
        }

        public static bool TryCast(this DbDataReader reader, int ordinal, Type castType, out object result)
        {
            result = null;
            if (!reader.HasRows || reader.IsDBNull(ordinal))
                return false;
            var type = reader.GetFieldType(ordinal);

            if (castType.IsEnum)
            {
                object enumValue;
                var castResult = DbReaderExtensionsData.CastEnum(reader, ordinal, type, castType, out enumValue);
                if (castResult)
                    result = enumValue;
                return castResult;
            }

            if (DbReaderExtensionsData.DbDataReaderCastMapping.ContainsKey(castType) && type == castType)
            {
                try
                {
                    result = DbReaderExtensionsData.DbDataReaderCastMapping[castType](reader, ordinal);
                    return true;
                }
                catch
                {
                    return false;
                }
            }
            var dbVal = reader.GetValue(ordinal);
            if (DbReaderExtensionsData.TypeConvertCastMapping.ContainsKey(castType))
            {
                try
                {
                    result = DbReaderExtensionsData.TypeConvertCastMapping[castType](dbVal);
                    return true;
                }
                catch
                {
                    return false;
                }
            }
            try
            {
                result = Convert.ChangeType(dbVal, castType);
                return true;
            }
            catch
            {
                return false;
            }
        }
    }
}